package com.newlink.building.userinfo.activity;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.YuvImage;
import android.media.Image;
import android.util.Base64;
import android.util.Log;
import android.util.Size;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.content.ContextCompat;


import com.google.android.gms.tasks.Task;

//import com.google.android.gms.vision.face.Face;
//import com.google.android.gms.vision.face.FaceDetector;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.gson.Gson;
import com.google.mlkit.vision.common.InputImage;
import com.google.mlkit.vision.face.Face;
import com.google.mlkit.vision.face.FaceDetection;
import com.google.mlkit.vision.face.FaceDetector;
//import com.google.mlkit.vision.face.FaceDetectorOptions;
import com.google.mlkit.vision.face.FaceDetectorOptions;
import com.newlink.building.common_base.base.NL_BaseActivity;
import com.newlink.building.common_base.utils.ToastUtils;
import com.newlink.building.userinfo.R;
import com.newlink.building.userinfo.helper.Module_UserInfo_SimilarityClassifier;


import org.tensorflow.lite.Interpreter;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.ReadOnlyBufferException;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class Module_UserInfo_FaceDetectionActivityNL extends NL_BaseActivity {

    Button mNext, mAddFace;
    HashMap<String, Module_UserInfo_SimilarityClassifier.Recognition> map = new HashMap<>();

    FaceDetector detector;
    private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
    PreviewView previewView;
    Interpreter tfLite;
    CameraSelector cameraSelector;
    ProcessCameraProvider cameraProvider;

    int[] intValues;
    int inputSize = 112;  //Input size for model
    boolean isModelQuantized = false;
    float[][] embeddings;
    float IMAGE_MEAN = 128.0f;
    float IMAGE_STD = 128.0f;
    //Output size of model
    int OUTPUT_SIZE = 192;

    String faceBase64;

    private static final int MY_CAMERA_REQUEST_CODE = 100;
    String modelFile = "mobile_face_net.tflite"; //model name


    @Nullable
    @Override
    protected View attachLayoutRes() {
        return getLayoutInflater().inflate(R.layout.activity_sign_up_first, null);
    }

    @Override
    public void initData() {

    }

    @Override
    public void initView() {
        //Function to initialize the variables
        Initialization();

        mAddFace.setVisibility(View.INVISIBLE);

        TextView tvCenter = findViewById(R.id.title_bar).findViewById(R.id.tv_title_bar_center);
        tvCenter.setText(getString(R.string.auth_face));

        // Camera Permission
        if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.CAMERA}, MY_CAMERA_REQUEST_CODE);
        }

        mNext.setOnClickListener(view -> {
            if (!AddFace2()) {
                return;
            }

            Log.e("jake", "当前人脸信息 :: " + mBase64Data);
            if (mBase64Data == null || mBase64Data.isEmpty()) {
                Log.e("jake", "当前人脸信息 111:: " + mBase64Data);
                ToastUtils.INSTANCE.showTextToast(getString(R.string.warning_load_face_first));
            } else {
                Log.e("jake", "当前人脸信息 222 :: " + mBase64Data);
                Intent intent = new Intent(this, Module_UserInfo_FaceDetectionActivityNL.class);
                intent.putExtra("face_data", mBase64Data);
                setResult(1_000_002, intent);
                finish();
            }
        });

        //Load model file
        try {
            tfLite = new Interpreter(loadModelFile(Module_UserInfo_FaceDetectionActivityNL.this, modelFile));
        } catch (IOException e) {
            ToastUtils.INSTANCE.showTextToast(this, "Error loading Interpreter....");
            Log.e("jake", "Error loading Interpreter...." + e);
            e.printStackTrace();
        }

        //Initialize Face Detector
        FaceDetectorOptions highAccuracyOpts = new FaceDetectorOptions.Builder()
                .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
                .setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL)
                .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
                .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
                .setMinFaceSize(0.2f)
                .build();
        detector = FaceDetection.getClient(highAccuracyOpts);

        cameraBind(); //Runs the camera
    }

    //Handles all the intent
    private void intentNow(Class targetActivity, String face_embeddings, String json, boolean b) {
        Intent intent = new Intent(getApplicationContext(), targetActivity);
        if (b) {
            //If b is true -> String json has some values (Called to goto 2nd Activity with json string)
            intent.putExtra(face_embeddings, json);
        }
        startActivity(intent);
        finish();
    }

    //Function to add face or store embeddings in a hasmap
    private void AddFace() {
        if (mCurrentFaceBitmap != null) {
            byte[] bytes = bitmap2Byte(mCurrentFaceBitmap);
            mBase64Data = byte2Base64(bytes);
            Log.e("jake", "[face detect] ===> base64 info is " + mBase64Data);
            ToastUtils.INSTANCE.showTextToast(this, getString(R.string.face_added_successfully));
//			mNext.setClickable(true);
        } else {
//			mNext.setClickable(false);
            ToastUtils.INSTANCE.showTextToast(this, getString(R.string.face_added_failed));
        }
    }

    private boolean AddFace2() {
        if (mCurrentFaceBitmap != null) {
            detector.close();
            byte[] bytes = bitmap2Byte(mCurrentFaceBitmap);
            mBase64Data = byte2Base64(bytes);

            Log.e("jake", "[face detect] ===> base64 info is " + mBase64Data);
            ToastUtils.INSTANCE.showTextToast(this, getString(R.string.face_added_successfully));
            return true;
        } else {
            ToastUtils.INSTANCE.showTextToast(this, getString(R.string.face_added_failed));
            return false;
        }

    }

    //Bind camera and preview view
    private void cameraBind() {
        cameraProviderFuture = ProcessCameraProvider.getInstance(this);
        previewView = findViewById(R.id.previewView);
        cameraProviderFuture.addListener(() -> {
            try {
                cameraProvider = cameraProviderFuture.get();

                bindPreview(cameraProvider); //Passes to bindPreview whatever is input of Camera
            } catch (ExecutionException | InterruptedException e) {
                // No errors need to be handled for this in Future.
                // This should never be reached.
            }
        }, ContextCompat.getMainExecutor(this));
    }

    void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
        Preview preview = new Preview.Builder().build();
        cameraSelector = new CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_FRONT).build();
        preview.setSurfaceProvider(previewView.getSurfaceProvider());

        ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
                .setTargetResolution(new Size(640, 480))
                //Latest frame is shown
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build();

        Executor executor = Executors.newSingleThreadExecutor();
        imageAnalysis.setAnalyzer(executor, imageProxy -> {
            try {
                Thread.sleep(10);  //Camera preview refreshed every 10 millis (adjust as required)
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            InputImage image = null;

            @SuppressLint({"UnsafeExperimentalUsageError", "UnsafeOptInUsageError"})
            // Camera Feed-->Analyzer-->ImageProxy-->mediaImage-->InputImage(needed for ML kit face detection)

            Image mediaImage = imageProxy.getImage();

            if (mediaImage != null) {
                image = InputImage.fromMediaImage(mediaImage, imageProxy.getImageInfo().getRotationDegrees());
            }

            //Process acquired image to detect faces
            assert image != null;
            Task<List<Face>> result = detector.process(image).addOnSuccessListener(faces -> {

                        if (faces.size() != 0) {

//							Face face = faces.get(0);
                            Face face = getCenterMostFace(faces, mediaImage.getWidth(), mediaImage.getHeight());

                            //mediaImage to Bitmap
                            Bitmap frame_bmp = toBitmap(mediaImage);

                            int rot = imageProxy.getImageInfo().getRotationDegrees();

                            //Adjust orientation of Face
                            Bitmap frame_bmp1 = rotateBitmap(frame_bmp, rot, false);

                            //Get bounding box of face
                            RectF boundingBox = new RectF(face.getBoundingBox());

                            //Crop out bounding box from whole Bitmap(image)
                            Bitmap cropped_face = getCropBitmapByCPU(frame_bmp1, boundingBox);

                            cropped_face = rotateBitmap(cropped_face, 0, true);

                            //Scale the acquired Face to 112*112 which is required input for model
                            Bitmap scaled = getResizedBitmap(cropped_face, inputSize, inputSize);

                            recognizeImage(scaled); //Send scaled bitmap to create face embeddings.

                        } else {
                            mCurrentFaceBitmap = null;
                            mAddFace.setVisibility(View.INVISIBLE); //If no face is detected -> Remove the 'Add Face' button from view
                        }

                    }).addOnFailureListener(e -> {    // Task failed with an exception
                    })

                    .addOnCompleteListener(task -> {
                        imageProxy.close(); //v.important to acquire next frame for analysis
                    });

        });

        cameraProvider.bindToLifecycle(this, cameraSelector, imageAnalysis, preview);

    }

    private Face getCenterMostFace(List<Face> faces, int imageWidth, int imageHeight) {
        // Calculate the center point of the image
        float imageCenterX = imageWidth / 2.0f;
        float imageCenterY = imageHeight / 2.0f;

        // Initialize the minimum distance with a large value
        double minDistance = Double.MAX_VALUE;
        Face centerMostFace = null;

        // Loop through all detected faces to find the center most face
        for (Face face : faces) {
            Rect boundingBox = face.getBoundingBox();
            float faceCenterX = boundingBox.centerX();
            float faceCenterY = boundingBox.centerY();

            // Calculate the Euclidean distance from the face center to the image center
            double distance = Math.sqrt(Math.pow(faceCenterX - imageCenterX, 2) + Math.pow(faceCenterY - imageCenterY, 2));

            // Check if this face is closer to the image center than the previous closest face
            if (distance < minDistance) {
                minDistance = distance;
                centerMostFace = face;
            }
        }

        return centerMostFace;
    }

    Bitmap mCurrentFaceBitmap;
    String mBase64Data;

    public void recognizeImage(final Bitmap bitmap) {
        //If the face is detected
        mAddFace.setVisibility(View.INVISIBLE);

        mCurrentFaceBitmap = bitmap;
        Log.e("facedetection", "[jake] ..  更新图片..");

        //Create ByteBuffer to store normalized image
        ByteBuffer imgData = ByteBuffer.allocateDirect(inputSize * inputSize * 3 * 4);

        imgData.order(ByteOrder.nativeOrder());

        intValues = new int[inputSize * inputSize];

        //get pixel values from Bitmap to normalize
        bitmap.getPixels(intValues, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());

        imgData.rewind();

        for (int i = 0; i < inputSize; ++i) {
            for (int j = 0; j < inputSize; ++j) {
                int pixelValue = intValues[i * inputSize + j];
                if (isModelQuantized) {
                    // Quantized model
                    imgData.put((byte) ((pixelValue >> 16) & 0xFF));
                    imgData.put((byte) ((pixelValue >> 8) & 0xFF));
                    imgData.put((byte) (pixelValue & 0xFF));
                } else { // Float model
                    imgData.putFloat((((pixelValue >> 16) & 0xFF) - IMAGE_MEAN) / IMAGE_STD);
                    imgData.putFloat((((pixelValue >> 8) & 0xFF) - IMAGE_MEAN) / IMAGE_STD);
                    imgData.putFloat(((pixelValue & 0xFF) - IMAGE_MEAN) / IMAGE_STD);

                }
            }
        }
        //imgData is input to our model
        Object[] inputArray = {imgData};

        Map<Integer, Object> outputMap = new HashMap<>();
        //output of model will be stored in this variable
        embeddings = new float[1][OUTPUT_SIZE];

        outputMap.put(0, embeddings);
        //Run model
        tfLite.runForMultipleInputsOutputs(inputArray, outputMap);
    }

    //Bitmap Processing
    public Bitmap getResizedBitmap(Bitmap bm, int newWidth, int newHeight) {
        int width = bm.getWidth();
        int height = bm.getHeight();
        float scaleWidth = ((float) newWidth) / width;
        float scaleHeight = ((float) newHeight) / height;
        // CREATE A MATRIX FOR THE MANIPULATION
        Matrix matrix = new Matrix();
        // RESIZE THE BIT MAP
        matrix.postScale(scaleWidth, scaleHeight);

        // "RECREATE" THE NEW BITMAP
        Bitmap resizedBitmap = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, false);
        bm.recycle();
        return resizedBitmap;
    }

    private static Bitmap getCropBitmapByCPU(Bitmap source, RectF cropRectF) {
        // Increase the size of the crop area by 100 pixels in all directions
        float extraSize = 158.0f;

        Bitmap resultBitmap = Bitmap.createBitmap((int) (cropRectF.width() + extraSize), (int) (cropRectF.height() + extraSize), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(resultBitmap);

        // draw background
        Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
        paint.setColor(Color.WHITE);
        canvas.drawRect(new RectF(0, 0, cropRectF.width() + extraSize, cropRectF.height() + extraSize), paint);

        Matrix matrix = new Matrix();
        matrix.postTranslate(-cropRectF.left + extraSize / 2, -cropRectF.top + extraSize / 2);

        canvas.drawBitmap(source, matrix, paint);

        if (source != null && !source.isRecycled()) {
            source.recycle();
        }

        return resultBitmap;
    }

    private static Bitmap rotateBitmap(Bitmap bitmap, int rotationDegrees, boolean flipX) {
        Matrix matrix = new Matrix();

        // Rotate the image back to straight.
        matrix.postRotate(rotationDegrees);

        // Mirror the image along the X or Y axis.
        matrix.postScale(flipX ? -1.0f : 1.0f, 1.0f);
        Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);

        // Recycle the old bitmap if it has changed.
        if (rotatedBitmap != bitmap) {
            bitmap.recycle();
        }
        return rotatedBitmap;
    }

    //IMPORTANT. If conversion not done ,the toBitmap conversion does not work on some devices.
    private static byte[] YUV_420_888toNV21(Image image) {

        int width = image.getWidth();
        int height = image.getHeight();
        int ySize = width * height;
        int uvSize = width * height / 4;

        byte[] nv21 = new byte[ySize + uvSize * 2];

        ByteBuffer yBuffer = image.getPlanes()[0].getBuffer(); // Y
        ByteBuffer uBuffer = image.getPlanes()[1].getBuffer(); // U
        ByteBuffer vBuffer = image.getPlanes()[2].getBuffer(); // V

        int rowStride = image.getPlanes()[0].getRowStride();
        assert (image.getPlanes()[0].getPixelStride() == 1);

        int pos = 0;

        if (rowStride == width) { // likely
            yBuffer.get(nv21, 0, ySize);
            pos += ySize;
        } else {
            long yBufferPos = -rowStride; // not an actual position
            for (; pos < ySize; pos += width) {
                yBufferPos += rowStride;
                yBuffer.position((int) yBufferPos);
                yBuffer.get(nv21, pos, width);
            }
        }

        rowStride = image.getPlanes()[2].getRowStride();
        int pixelStride = image.getPlanes()[2].getPixelStride();

        assert (rowStride == image.getPlanes()[1].getRowStride());
        assert (pixelStride == image.getPlanes()[1].getPixelStride());

        if (pixelStride == 2 && rowStride == width && uBuffer.get(0) == vBuffer.get(1)) {
            // maybe V an U planes overlap as per NV21, which means vBuffer[1] is alias of uBuffer[0]
            byte savePixel = vBuffer.get(1);
            try {
                vBuffer.put(1, (byte) ~savePixel);
                if (uBuffer.get(0) == (byte) ~savePixel) {
                    vBuffer.put(1, savePixel);
                    vBuffer.position(0);
                    uBuffer.position(0);
                    vBuffer.get(nv21, ySize, 1);
                    uBuffer.get(nv21, ySize + 1, uBuffer.remaining());

                    return nv21; // shortcut
                }
            } catch (ReadOnlyBufferException ex) {
                // unfortunately, we cannot check if vBuffer and uBuffer overlap
            }

            // unfortunately, the check failed. We must save U and V pixel by pixel
            vBuffer.put(1, savePixel);
        }

        // other optimizations could check if (pixelStride == 1) or (pixelStride == 2),
        // but performance gain would be less significant

        for (int row = 0; row < height / 2; row++) {
            for (int col = 0; col < width / 2; col++) {
                int vuPos = col * pixelStride + row * rowStride;
                nv21[pos++] = vBuffer.get(vuPos);
                nv21[pos++] = uBuffer.get(vuPos);
            }
        }

        return nv21;
    }

    private Bitmap toBitmap(Image image) {

        byte[] nv21 = YUV_420_888toNV21(image);

        YuvImage yuvImage = new YuvImage(nv21, ImageFormat.NV21, image.getWidth(), image.getHeight(), null);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        yuvImage.compressToJpeg(new Rect(0, 0, yuvImage.getWidth(), yuvImage.getHeight()), 100, out);

        byte[] imageBytes = out.toByteArray();
        return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
    }

    //Convert hashmap to string, basically to pass and store it in firebase
    private String getFromMap(HashMap<String, Module_UserInfo_SimilarityClassifier.Recognition> json) {
        return new Gson().toJson(json);
    }

    //Initializing all the variables
    private void Initialization() {

        mNext = findViewById(R.id.next);
        mAddFace = findViewById(R.id.addButton);
    }

    //To ask permission for camera
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == MY_CAMERA_REQUEST_CODE) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED)
                ToastUtils.INSTANCE.showTextToast(this, "Camera permission granted");
            else {
                ToastUtils.INSTANCE.showTextToast(this, "Camera permission denied");
            }
        }
    }

    //Loads model file
    private MappedByteBuffer loadModelFile(Activity activity, String MODEL_FILE) throws IOException {
        AssetFileDescriptor fileDescriptor = activity.getAssets().openFd(MODEL_FILE);
        FileInputStream inputStream = new FileInputStream(fileDescriptor.getFileDescriptor());
        FileChannel fileChannel = inputStream.getChannel();
        long startOffset = fileDescriptor.getStartOffset();
        long declaredLength = fileDescriptor.getDeclaredLength();
        return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength);
    }

    public static byte[] bitmap2Byte(Bitmap bitmap) {
        if (null == bitmap) throw new NullPointerException();
        // if (null == bitmap) return null;
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        //把bitmap100%高质量压缩 到 output对象里
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
        return outputStream.toByteArray();
    }

    public static String byte2Base64(byte[] imageByte) {
        if (null == imageByte) return null;
        return Base64.encodeToString(imageByte, Base64.DEFAULT);
    }
}
